/*
 * File     : ScratchMemory.java
 * Created  : 4 May 2011
 *
 * Copyright © 2011 Matthew Wilson (mj. {my-surname} .uk {at} gmail.com)
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.googlecode.dni.internal.library;

import java.nio.ByteBuffer;
import java.util.logging.Level;

import com.googlecode.dni.internal.DniInternal;
import com.googlecode.dni.type.Pointer;

/**
 * <p>
 *  Encapsulates scratch memory for library calls.
 * </p>
 * <p>
 *  This class acts as a sort of stack, because Java doesn't have access to the
 *  native stack.  (And rightly so.)  It's thread local.
 * </p>
 * <h3>Usage model</h3>
 * <pre>
 *   ScratchMemory scratchMemory = ScratchMemory.reserve( 128 );
 *   long ptr = scratchMemory.getPointer64();
 *   ByteBuffer buffer = scratchMemory.getBuffer( 128 );
 *   try
 *   {
 *       buffer.putInt( 42 ); // etc.
 *
 *       someNativeMethod( ptr );
 *   }
 *   finally
 *   {
 *       scratchMemory.release( 128 );
 *   }
 * </pre>
 *
 * @author Matthew Wilson
 */
public final class ScratchMemory
{

    // TODO this should be configurable
    private static final int DEFAULT_SIZE = 256 * 1024;

    private static final ThreadLocal< ScratchMemory > INSTANCE = new ThreadLocal< ScratchMemory >()
    {
        @Override
        protected ScratchMemory initialValue()
        {
            return new ScratchMemory();
        }
    };


    /** Pointer to the base of the buffer. */
    private final long basePointer;

    /** Bytes allocated. */
    private final int allocatedSize;

    /** Buffer of the space. */
    private final ByteBuffer buffer;

    private int position;

    // TODO support a debug mode that does more safety checking


    private ScratchMemory()
    {
        this.allocatedSize = DEFAULT_SIZE;
        Pointer pointer = DniInternal.PLATFORM.allocatePages( this.allocatedSize, false );
        this.basePointer = pointer.getAddress();
        this.buffer = pointer.getBuffer( this.allocatedSize );
    }

    /**
     * Obtains the scratch memory for this thread, reserving the given number of
     * bytes.
     *
     * @param size
     *            the size in bytes to reserve
     * @return the scratch memory
     */
    public static ScratchMemory reserve( final int size )
    {
        assert ( size & 31 ) == 0 : size;

        ScratchMemory scratchMemory = INSTANCE.get();

        if ( scratchMemory.allocatedSize - scratchMemory.position < size )
        {
            throw new OutOfMemoryError( "OK, I need to implement increasing the scratch memory size" );
        }

        return scratchMemory;
    }

    /**
     * Obtains the pointer to the current scratch memory region.
     *
     * @return the pointer (for 64-bit address space)
     */
    public long getPointer64()
    {
        return this.basePointer + this.position;
    }

    /**
     * Obtains the pointer to the current scratch memory region.
     *
     * @return the pointer (for 32-bit address space)
     */
    public int getPointer32()
    {
        return (int) this.basePointer + this.position;
    }

    /**
     * Obtains the buffer for the reserved memory.
     *
     * @param size
     *            the size in bytes reserved
     *
     * @return a buffer; use the buffer only once
     */
    public ByteBuffer getBuffer( final int size )
    {
        this.buffer.clear();
        this.buffer.position( this.position );

        this.position += size;
        this.buffer.limit( this.position );

        return this.buffer;
    }

    /**
     * Release memory that was reserved and used.
     *
     * @param size
     *            the size in bytes reserved
     */
    public void release( final int size )
    {
        this.position -= size;
        assert this.position >= 0;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void finalize() throws Throwable
    {
        try
        {
            DniInternal.PLATFORM.unallocatePages( new Pointer( this.basePointer ), this.allocatedSize );
        }
        catch ( Throwable throwable )
        {
            DniInternal.LOGGER.log( Level.SEVERE, "Failed to free scratch memory", throwable );
        }
    }

}
